#!/usr/bin/perl

# Copyright (C) Intel, Inc.
# (C) Maxim Dounin
# (C) Sergey Kandaurov
# (C) Nginx, Inc.

# Tests for unbuffered request body with fastcgi backend,
# chunked transfer-encoding.

###############################################################################

use warnings;
use strict;

use Test::More;
use Socket qw/ CRLF /;

BEGIN { use FindBin; chdir($FindBin::Bin); }

use lib 'lib';
use Test::Nginx;

###############################################################################

select STDERR; $| = 1;
select STDOUT; $| = 1;

eval { require FCGI; };
plan(skip_all => 'FCGI not installed') if $@;
plan(skip_all => 'win32') if $^O eq 'MSWin32';

my $t = Test::Nginx->new()->has(qw/http fastcgi rewrite/)->plan(19);

$t->write_file_expand('nginx.conf', <<'EOF');

%%TEST_GLOBALS%%

daemon off;

events {
}

http {
    %%TEST_GLOBALS_HTTP%%

    server {
        listen       127.0.0.1:8080;
        server_name  localhost;

        client_header_buffer_size 1k;
        fastcgi_request_buffering off;
        fastcgi_param REQUEST_URI $request_uri;

        location / {
            client_body_buffer_size 2k;
            fastcgi_pass 127.0.0.1:8081;
        }
        location /single {
            client_body_in_single_buffer on;
            fastcgi_pass 127.0.0.1:8081;
        }
        location /preread {
            fastcgi_pass 127.0.0.1:8082;
        }
        location /error_page {
            fastcgi_pass 127.0.0.1:8081;
            error_page 404 /404;
            fastcgi_intercept_errors on;
        }
        location /404 {
            return 200 "$request_body\n";
        }
    }
}

EOF

$t->run_daemon(\&fastcgi_daemon);
$t->run()->waitforsocket('127.0.0.1:' . port(8081));

###############################################################################

like(http_get('/'), qr/X-Body: \x0d\x0a?/ms, 'no body');

like(http_get_body('/', '0123456789'),
    qr/X-Body: 0123456789\x0d?$/ms, 'body');

like(http_get_body('/', '0123456789' x 128),
    qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in two buffers');

like(http_get_body('/single', '0123456789' x 128),
    qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in single buffer');

like(http_get_body('/error_page', '0123456789'),
    qr/^0123456789$/m, 'body in error page');

# pipelined requests

like(http_get_body('/', '0123456789', '0123456789' x 128, '0123456789' x 512,
    'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined');
like(http_get_body('/', '0123456789' x 128, '0123456789' x 512, '0123456789',
    'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined 2');

# interactive tests

my $s = get_body('/preread', port(8082));
ok($s, 'no preread');

SKIP: {
skip 'no preread failed', 3 unless $s;

is($s->{upload}('01234'), '01234', 'no preread - body part');
is($s->{upload}('56789', last => 1), '56789', 'no preread - body part 2');

like($s->{http_end}(), qr/200 OK/, 'no preread - response');

}

$s = get_body('/preread', port(8082), '01234');
ok($s, 'preread');

SKIP: {
skip 'preread failed', 3 unless $s;

is($s->{preread}, '01234', 'preread - preread');
is($s->{upload}('56789', last => 1), '56789', 'preread - body');

like($s->{http_end}(), qr/200 OK/, 'preread - response');

}

$s = get_body('/preread', port(8082), '01234', many => 1);
ok($s, 'chunks');

SKIP: {
skip 'chunks failed', 3 unless $s;

is($s->{preread}, '01234many', 'chunks - preread');
is($s->{upload}('56789', many => 1, last => 1), '56789many', 'chunks - body');

like($s->{http_end}(), qr/200 OK/, 'chunks - response');

}

###############################################################################

sub http_get_body {
    my $uri = shift;
    my $last = pop;
    return http( join '', (map {
        my $body = $_;
        "GET $uri HTTP/1.1" . CRLF
        . "Host: localhost" . CRLF
        . "Transfer-Encoding: chunked" . CRLF . CRLF
        . sprintf("%x", length $body) . CRLF
        . $body . CRLF
        . "0" . CRLF . CRLF
    } @_),
        "GET $uri HTTP/1.1" . CRLF
        . "Host: localhost" . CRLF
        . "Connection: close" . CRLF
        . "Transfer-Encoding: chunked" . CRLF . CRLF
        . sprintf("%x", length $last) . CRLF
        . $last . CRLF
        . "0" . CRLF . CRLF
    );
}

# Simple FastCGI responder implementation.

# http://www.fastcgi.com/devkit/doc/fcgi-spec.html

sub fastcgi_read_record($) {
    my ($buf) = @_;
    my $h;

    return undef unless length $$buf;

    @{$h}{qw/ version type id clen plen /} = unpack("CCnnC", $$buf);

    $h->{content} = substr $$buf, 8, $h->{clen};
    $h->{padding} = substr $$buf, 8 + $h->{clen}, $h->{plen};

    $$buf = substr $$buf, 8 + $h->{clen} + $h->{plen};

    return $h;
}

sub fastcgi_respond($$$$) {
    my ($socket, $version, $id, $body) = @_;

    # stdout
    $socket->write(pack("CCnnCx", $version, 6, $id, length($body), 8));
    $socket->write($body);
    select(undef, undef, undef, 0.1);
    $socket->write(pack("xxxxxxxx"));
    select(undef, undef, undef, 0.1);

    # write some text to stdout and stderr split over multiple network
    # packets to test if we correctly set pipe length in various places

    my $tt = "test text, just for test";

    $socket->write(pack("CCnnCx", $version, 6, $id,
        length($tt . $tt), 0) . $tt);
    select(undef, undef, undef, 0.1);
    $socket->write($tt . pack("CC", $version, 7));
    select(undef, undef, undef, 0.1);
    $socket->write(pack("nnCx", $id, length($tt), 0));
    select(undef, undef, undef, 0.1);
    $socket->write($tt);
    select(undef, undef, undef, 0.1);

    # close stdout
    $socket->write(pack("CCnnCx", $version, 6, $id, 0, 0));

    select(undef, undef, undef, 0.1);

    # end request
    $socket->write(pack("CCnnCx", $version, 3, $id, 8, 0));
    select(undef, undef, undef, 0.1);
    $socket->write(pack("NCxxx", 0, 0));
}

sub get_body {
    my ($url, $port, $body, %extra) = @_;
    my ($server, $client, $s);
    my ($last, $many) = (0, 0);
    my ($version, $id);

    $last = $extra{last} if defined $extra{last};
    $many = $extra{many} if defined $extra{many};

    $server = IO::Socket::INET->new(
        Proto => 'tcp',
        LocalHost => '127.0.0.1',
        LocalPort => $port,
        Listen => 5,
        Reuse => 1
    )
        or die "Can't create listening socket: $!\n";

    my $r = <<EOF;
GET $url HTTP/1.1
Host: localhost
Connection: close
Transfer-Encoding: chunked

EOF

    if (defined $body) {
        $r .= sprintf("%x", length $body) . CRLF;
        $r .= $body . CRLF;
    }
    if (defined $body && $many) {
        $r .= sprintf("%x", length 'many') . CRLF;
        $r .= 'many' . CRLF;
    }
    if ($last) {
        $r .= "0" . CRLF . CRLF;
    }

    $s = http($r, start => 1);

    eval {
        local $SIG{ALRM} = sub { die "timeout\n" };
        local $SIG{PIPE} = sub { die "sigpipe\n" };
        alarm(5);

        $client = $server->accept();

        log2c("(new connection $client)");

        alarm(0);
    };
    alarm(0);
    if ($@) {
        log_in("died: $@");
        return undef;
    }

    $client->sysread(my $buf, 1024);
    log2i($buf);

    $body = '';

    while (my $h = fastcgi_read_record(\$buf)) {
        $version = $h->{version};
        $id = $h->{id};

        # skip everything unless stdin
        next if $h->{type} != 5;

        $body .= $h->{content};
    }

    my $f = { preread => $body };
    $f->{upload} = sub {
        my ($body, %extra) = @_;
        my ($last, $many) = (0, 0);

        $last = $extra{last} if defined $extra{last};
        $many = $extra{many} if defined $extra{many};

        my $buf = sprintf("%x", length $body) . CRLF;
        $buf .= $body . CRLF;
        if ($many) {
            $buf .= sprintf("%x", length 'many') . CRLF;
            $buf .= 'many' . CRLF;
        }
        if ($last) {
            $buf .= "0" . CRLF . CRLF;
        }

        eval {
            local $SIG{ALRM} = sub { die "timeout\n" };
            local $SIG{PIPE} = sub { die "sigpipe\n" };
            alarm(5);

            log_out($buf);
            $s->write($buf);

            $client->sysread($buf, 1024);
            log2i($buf);

            $body = '';

            while (my $h = fastcgi_read_record(\$buf)) {

                # skip everything unless stdin
                next if $h->{type} != 5;

                $body .= $h->{content};
            }

            alarm(0);
        };
        alarm(0);
        if ($@) {
            log_in("died: $@");
            return undef;
        }

        return $body;
    };
    $f->{http_end} = sub {
        my $buf = '';

        eval {
            local $SIG{ALRM} = sub { die "timeout\n" };
            local $SIG{PIPE} = sub { die "sigpipe\n" };
            alarm(5);

            fastcgi_respond($client, $version, $id, <<EOF);
Status: 200 OK
Connection: close
X-Port: $port

OK
EOF

            $client->close;

            $s->sysread($buf, 1024);
            log_in($buf);

            $s->close();

            alarm(0);
        };
        alarm(0);
        if ($@) {
            log_in("died: $@");
            return undef;
        }

        return $buf;
    };
    return $f;
}

sub log2i { Test::Nginx::log_core('|| <<', @_); }
sub log2o { Test::Nginx::log_core('|| >>', @_); }
sub log2c { Test::Nginx::log_core('||', @_); }

###############################################################################

sub fastcgi_daemon {
    my $socket = FCGI::OpenSocket('127.0.0.1:' . port(8081), 5);
    my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV,
        $socket);

    my $count;
    my ($body, $buf);

    while( $request->Accept() >= 0 ) {
        $count++;
        $body = '';

        do {
            read(STDIN, $buf, 1024);
            $body .= $buf;
        } while (length $buf);

        if ($ENV{REQUEST_URI} eq '/error_page') {
            print "Status: 404 Not Found" . CRLF . CRLF;
            next;
        }

        print <<EOF;
Location: http://localhost/redirect
Content-Type: text/html
X-Body: $body

SEE-THIS
$count
EOF
    }

    FCGI::CloseSocket($socket);
}

###############################################################################
